/*
 * Copyright 2002-2010 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

using System.Reflection;
using System.Text;
using AopAlliance.Intercept;
using Microsoft.Extensions.Logging;

namespace Spring.Aspects.Logging;

/// <summary>
/// Configurable advice for logging.
/// </summary>
/// <remarks>
///
/// </remarks>
/// <author>Mark Pollack</author>
[Serializable]
public class SimpleLoggingAdvice : AbstractLoggingAdvice
{
    /// <summary>
    /// Flag to indicate if unique identifier should be in the log message.
    /// </summary>
    private bool logUniqueIdentifier;

    /// <summary>
    /// Flag to indicate if the execution time should be in the log message.
    /// </summary>
    private bool logExecutionTime;

    /// <summary>
    /// Flag to indicate if the method arguments should be in the log message.
    /// </summary>
    private bool logMethodArguments;

    /// <summary>
    /// Flag to indicate if the return value should be in the log message.
    /// </summary>
    private bool logReturnValue;

    /// <summary>
    /// The separator string to use for delmiting log message fields.
    /// </summary>
    private string separator = ", ";

    /// <summary>
    /// The log level to use for logging the entry, exit, exception messages.
    /// </summary>
    private LogLevel logLevel = LogLevel.Trace;

    /// <summary>
    /// Initializes a new instance of the <see cref="SimpleLoggingAdvice"/> class.
    /// </summary>
    public SimpleLoggingAdvice()
    {
    }

    /// <summary>
    /// Initializes a new instance of the <see cref="SimpleLoggingAdvice"/> class.
    /// </summary>
    /// <param name="useDynamicLogger">if set to <c>true</c> to use dynamic logger, if
    /// <c>false</c> use static logger.</param>
    public SimpleLoggingAdvice(bool useDynamicLogger)
    {
        UseDynamicLogger = useDynamicLogger;
    }

    /// <summary>
    /// Initializes a new instance of the <see cref="SimpleLoggingAdvice"/> class.
    /// </summary>
    /// <param name="defaultLogger">the default logger to use</param>
    public SimpleLoggingAdvice(ILogger defaultLogger)
        : base(defaultLogger)
    {
    }

    /// <summary>
    /// Gets or sets a value indicating whether to log a unique identifier with the log message.
    /// </summary>
    /// <value><c>true</c> if [log unique identifier]; otherwise, <c>false</c>.</value>
    public bool LogUniqueIdentifier
    {
        get { return logUniqueIdentifier; }
        set { logUniqueIdentifier = value; }
    }

    /// <summary>
    /// Gets or sets a value indicating whether to log execution time.
    /// </summary>
    /// <value><c>true</c> if log execution time; otherwise, <c>false</c>.</value>
    public bool LogExecutionTime
    {
        get { return logExecutionTime; }
        set { logExecutionTime = value; }
    }

    /// <summary>
    /// Gets or sets a value indicating whether log method arguments.
    /// </summary>
    /// <value><c>true</c> if log method arguments]; otherwise, <c>false</c>.</value>
    public bool LogMethodArguments
    {
        get { return logMethodArguments; }
        set { logMethodArguments = value; }
    }

    /// <summary>
    /// Gets or sets a value indicating whether log return value.
    /// </summary>
    /// <value><c>true</c> if log return value; otherwise, <c>false</c>.</value>
    public bool LogReturnValue
    {
        get { return logReturnValue; }
        set { logReturnValue = value; }
    }

    /// <summary>
    /// Gets or sets the seperator string to use for delmiting log message fields.
    /// </summary>
    /// <value>The seperator.</value>
    public string Separator
    {
        get { return separator; }
        set { separator = value; }
    }

    /// <summary>
    /// Gets or sets the entry log level.
    /// </summary>
    /// <value>The entry log level.</value>
    public LogLevel LogLevel
    {
        get { return logLevel; }
        set { logLevel = value; }
    }

    /// <summary>
    /// Subclasses must override this method to perform any tracing around the supplied
    /// IMethodInvocation.
    /// </summary>
    /// <param name="invocation">The method invocation to log</param>
    /// <param name="log">The log to write messages to</param>
    /// <returns>
    /// The result of the call to IMethodInvocation.Proceed()
    /// </returns>
    /// <remarks>
    /// Subclasses are resonsible for ensuring that the IMethodInvocation actually executes
    /// by calling IMethodInvocation.Proceed().
    /// <para>
    /// By default, the passed-in ILog instance will have log level
    /// "trace" enabled. Subclasses do not have to check for this again, unless
    /// they overwrite the IsInterceptorEnabled method to modify
    /// the default behavior.
    /// </para>
    /// </remarks>
    /// <exception cref="System.Exception">
    /// If any of the interceptors in the chain or the target object itself
    /// throws an exception.
    /// </exception>
    protected override object InvokeUnderLog(IMethodInvocation invocation, ILogger log)
    {
        object returnValue = null;
        bool exitThroughException = false;

        DateTime startTime = DateTime.Now;
        string uniqueIdentifier = null;

        if (LogUniqueIdentifier)
        {
            uniqueIdentifier = CreateUniqueIdentifier();
        }

        try
        {
            WriteToLog(LogLevel, log, GetEntryMessage(invocation, uniqueIdentifier), null);
            returnValue = invocation.Proceed();
            return returnValue;
        }
        catch (Exception e)
        {
            TimeSpan executionTimeSpan = DateTime.Now - startTime;
            WriteToLog(LogLevel, log, GetExceptionMessage(invocation, e, executionTimeSpan, uniqueIdentifier), e);
            exitThroughException = true;
            throw;
        }
        finally
        {
            if (!exitThroughException)
            {
                TimeSpan executionTimeSpan = DateTime.Now - startTime;
                WriteToLog(LogLevel, log, GetExitMessage(invocation, returnValue, executionTimeSpan, uniqueIdentifier), null);
            }
        }
    }

    /// <summary>
    /// Determines whether the given log is enabled.
    /// </summary>
    /// <param name="log">The log instance to check.</param>
    /// <returns>
    /// 	<c>true</c> if log is for a given log level; otherwise, <c>false</c>.
    /// </returns>
    /// <remarks>
    /// Default is true when the trace level is enabled.  Subclasses may override this
    /// to change the level at which logging occurs, or return true to ignore level
    /// checks.</remarks>
    protected override bool IsLogEnabled(ILogger log)
    {
        switch (LogLevel)
        {
            case LogLevel.Trace:
                if (log.IsEnabled(LogLevel.Trace))
                {
                    return true;
                }

                break;
            case LogLevel.Debug:
                if (log.IsEnabled(LogLevel.Debug))
                {
                    return true;
                }

                break;
            case LogLevel.Error:
                if (log.IsEnabled(LogLevel.Error))
                {
                    return true;
                }

                break;
            case LogLevel.Critical:
                if (log.IsEnabled(LogLevel.Critical))
                {
                    return true;
                }

                break;
            case LogLevel.Information:
                if (log.IsEnabled(LogLevel.Information))
                {
                    return true;
                }

                break;
            case LogLevel.Warning:
                if (log.IsEnabled(LogLevel.Warning))
                {
                    return true;
                }

                break;
            case LogLevel.None:
            default:
                break;
        }

        return false;
    }

    /// <summary>
    /// Creates a unique identifier.
    /// </summary>
    /// <remarks>
    /// Default implementation uses Guid.NewGuid().  Subclasses may override to provide an alternative
    /// ID generation implementation.
    /// </remarks>
    /// <returns>A unique identifier</returns>
    protected virtual string CreateUniqueIdentifier()
    {
        return Guid.NewGuid().ToString();
    }

    /// <summary>
    /// Gets the entry message to log
    /// </summary>
    /// <param name="invocation">The invocation.</param>
    /// <param name="idString">The id string.</param>
    /// <returns>The entry log message</returns>
    protected virtual string GetEntryMessage(IMethodInvocation invocation, string idString)
    {
        StringBuilder sb = new StringBuilder(128);
        sb.Append("Entering ");
        AppendCommonInformation(sb, invocation, idString);
        if (logMethodArguments)
        {
            sb.Append(GetMethodArgumentAsString(invocation));
        }

        return RemoveLastSeparator(sb.ToString(), Separator);
    }

    /// <summary>
    /// Gets the exception message.
    /// </summary>
    /// <param name="invocation">The method invocation.</param>
    /// <param name="e">The thown exception.</param>
    /// <param name="executionTimeSpan">The execution time span.</param>
    /// <param name="idString">The id string.</param>
    /// <returns>The exception log message.</returns>
    protected virtual string GetExceptionMessage(IMethodInvocation invocation, Exception e, TimeSpan executionTimeSpan, string idString)
    {
        StringBuilder sb = new StringBuilder(128);
        sb.Append("Exception thrown in ");
        sb.Append(invocation.Method.Name).Append(Separator);
        AppendCommonInformation(sb, invocation, idString);
        if (LogExecutionTime)
        {
            sb.Append(executionTimeSpan.TotalMilliseconds).Append(" ms");
        }

        return RemoveLastSeparator(sb.ToString(), Separator);
    }

    /// <summary>
    /// Gets the exit log message.
    /// </summary>
    /// <param name="invocation">The method invocation.</param>
    /// <param name="returnValue">The return value.</param>
    /// <param name="executionTimeSpan">The execution time span.</param>
    /// <param name="idString">The id string.</param>
    /// <returns>the exit log message</returns>
    protected virtual string GetExitMessage(IMethodInvocation invocation, object returnValue, TimeSpan executionTimeSpan, string idString)
    {
        StringBuilder sb = new StringBuilder(128);
        sb.Append("Exiting ");
        AppendCommonInformation(sb, invocation, idString);
        if (LogReturnValue && invocation.Method.ReturnType != typeof(void))
        {
            sb.Append("return=").Append(returnValue).Append(Separator);
        }

        if (LogExecutionTime)
        {
            sb.Append(executionTimeSpan.TotalMilliseconds).Append(" ms");
        }

        return RemoveLastSeparator(sb.ToString(), Separator);
    }

    /// <summary>
    /// Appends common information across entry,exit, exception logging
    /// </summary>
    /// <remarks>Add method name and unique identifier if required.</remarks>
    /// <param name="sb">The string buffer building logging message.</param>
    /// <param name="invocation">The method invocation.</param>
    /// <param name="idString">The unique identifier string.</param>
    protected virtual void AppendCommonInformation(StringBuilder sb, IMethodInvocation invocation, string idString)
    {
        sb.Append(invocation.Method.Name);
        if (LogUniqueIdentifier)
        {
            sb.Append(Separator).Append(idString);
        }

        sb.Append(Separator);
    }

    /// <summary>
    /// Gets the method argument as argumen name/value pairs.
    /// </summary>
    /// <param name="invocation">The method invocation.</param>
    /// <returns>string for logging method argument name and values.</returns>
    protected virtual string GetMethodArgumentAsString(IMethodInvocation invocation)
    {
        StringBuilder sb = new StringBuilder(128);
        ParameterInfo[] parameterInfos = invocation.Method.GetParameters();
        object[] argValues = invocation.Arguments;
        for (int i = 0; i < parameterInfos.Length; i++)
        {
            sb.Append(parameterInfos[i].Name).Append("=").Append(argValues[i]);
            if (i != parameterInfos.Length) sb.Append("; ");
        }

        return RemoveLastSeparator(sb.ToString(), "; ");
    }

    private string RemoveLastSeparator(string str, string separator)
    {
        if (str.EndsWith(separator))
        {
            return str.Substring(0, str.Length - separator.Length);
        }
        else
        {
            return str;
        }
    }

    private void WriteToLog(LogLevel logLevel, ILogger log, string text, Exception e)
    {
        switch (logLevel)
        {
            case LogLevel.Trace:
                if (log.IsEnabled(LogLevel.Trace))
                {
                    if (e == null) log.LogTrace(text);
                    else log.LogTrace(e, text);
                }

                break;
            case LogLevel.Debug:
                if (log.IsEnabled(LogLevel.Debug))
                {
                    if (e == null) log.LogDebug(text);
                    else log.LogDebug(e, text);
                }

                break;
            case LogLevel.Error:
                if (log.IsEnabled(LogLevel.Error))
                {
                    if (e == null) log.LogError(text);
                    else log.LogError(e, text);
                }

                break;
            case LogLevel.Critical:
                if (log.IsEnabled(LogLevel.Critical))
                {
                    if (e == null) log.LogCritical(text);
                    else log.LogCritical(e, text);
                }

                break;
            case LogLevel.Information:
                if (log.IsEnabled(LogLevel.Information))
                {
                    if (e == null) log.LogInformation(text);
                    else log.LogInformation(e, text);
                }

                break;
            case LogLevel.Warning:
                if (log.IsEnabled(LogLevel.Warning))
                {
                    if (e == null) log.LogWarning(text);
                    else log.LogWarning(e, text);
                }

                break;
            case LogLevel.None:
            default:
                break;
        }
    }
}